できた!S3 オリジンへの直接アクセス制限と、インデックスドキュメント機能を共存させる方法
ちょっと伝わりにくいタイトルですが、やりたいことは以下の2つです。
- CloudFront の S3 オリジンには直接アクセスさせない(CloudFront をバイパスした S3 へのアクセスをブロック)
- オブジェクト指定のないアクセス(末尾"/"の URL アクセス)にはインデックスドキュメントを返す(サブディレクトリも含む)
画にするとこういうことです↓
「CloudFront をバイパスさせない」という点でまず考えるのは、オリジンアクセスアイデンティティでかと思います。そして、「オブジェクト指定のないアクセスにインデックスドキュメントを返す」という点で思いつくのは、Default Root Object ですね。最初に考えたときは、この2つの機能で実現できるだろうと思ってたのですが、そうは問屋が卸してくれなかったです。。
なぜなら、CloudFront の Default Root Object
設定は、ディストリビューションのルート URL のみが対象であり、サブディレクトリには効きません。つまり、以下のような動作になるということです。
- 例:Default Root Object に index.html を定義した場合
- http://exsample.com/ → index.html を返す
- http://exsample.com/subdir/ → index.html は返りません
Default Root Object
では対応できないため、次に考えるのは S3 静的ホスティングのインデックスドキュメント設定ですね。これならばサブディレクトリに対しても、インデックスドキュメントを返すことができるのですが、その場合、S3 オリジンではなく、カスタムオリジンとして S3 のウェブサイトエンドポイントに変更する必要があるため、オリジンアクセスアイデンティティを使用した CloudFront のバイパス制限を実装することが出来なくなります。
ではどうしましょう、、。S3 バケットポリシーで、CloudFront に割り当てられる可能性のある IP アドレス範囲を片っ端からアクセス許可すれば可能かもしれませんが、どれだけの IP アドレスを定義する必要があるでしょうか。また、それらの IP アドレス範囲の不定期な変更をチェックし、変更があった場合は S3 バケットポリシーに反映するような仕組みが必要になるでしょう。現実的にはあまり採用したくない方法ですね。
そこでご紹介するのが、Lambda@Edgeです。
Lambda@Edge の出番ですよ!
Lambda@Edge は 2016年の Re:Invent で発表された、「CloudFront のエッジロケーションで Lambda を実行する」サービスになります。
CloudFront ディストリビューションに Lambda 関数に関連付けると、CloudFront エッジロケーションでリクエストとレスポンスが傍受することができ、次の CloudFront イベントの発生時に Lambda 関数を実行できます。
- CloudFront がビューワーからリクエストを受信したとき (ビューワーリクエスト)
- CloudFront がリクエストをオリジンに転送する前 (オリジンリクエスト)
- CloudFront がオリジンからレスポンスを受信したとき (オリジンレスポンス)
- CloudFront がビューワーにレスポンスを返す前 (ビューワーレスポンス)
つまり、どゆこと?
今回のケースならば、オリジンリクエスト
をトリガーに Lambda@Edge 関数を実行させて、オブジェクト指定のない URL (末尾が "/" で終わっている)アクセスを、index.html を付与した URL パスに書き換えて S3 オリジンにリクエストするようにしてしままえば良い、ということです。
画にするとこんな感じです↓
この構成であれば、そもそも Default Root Object
の指定は必要ありません。そして、S3 静的ウェブサイトホスティングを有効にする必要もないため、オリジンアクセスアイデンティティ
が利用できますので、CloudFront をバイパスしたアクセスを制限することも可能です!
早速やってみる!
S3 バケットとオブジェクトの準備
S3 バケットの直下と、subdir/
に index.html
を準備しておきます。なお、静的ウェブサイトホスティングは設定しません。
- s3://cm-rootobject-test/index.html →
root index!
と表示。 - s3://cm-rootobject-test/subdir/index.html →
subdir index!
と表示。
CloudFront を作成
CloudFront ディストリビューションを作成します。 CloudFront コンソールを開き、Create Distribution
を選択します。Select a delivery method for your content
では Web
の Get Started
を選択します。
- Origin Domain Name: 先ほど作成した S3 バケットを選択
- Restrict Bucket Access:
Yes
を選択 - Origin Access Identity:
Create a new identity
を選択 - Grant Read Permissions on Bucket:
Yes, Update Bucket Policy
を選択
- Object Caching:
Customize
を選択。 - Minimum TTL: 0
- Maximum TTL: 0
- Default TTL: 0
今回はテストを容易にするために、キャッシュさせない設定としましたが、実際に利用されるならば、適切にキャッシュを設定したほうが良いでしょう。(じゃないと、毎回 Lambda@Edge 関数が起動しますので、、)その他は、デフォルトを受け入れてます。もちろん Default Root Object
の定義もしておりません。
Lambda@Edge がないときぃ〜
まずは、Lambda@Edge 関数を作成する前の状態を確認してみましょう。先ほど作成した CloudFront の Domain Name
に対して /
と /subdir/
でアクセスしてみます。
$ curl -I http://dz00r977zpll6.cloudfront.net/ HTTP/1.1 403 Forbidden Content-Type: application/xml Connection: keep-alive x-amz-bucket-region: ap-northeast-1 Date: Tue, 24 Apr 2018 14:43:47 GMT Server: AmazonS3 X-Cache: Error from cloudfront Via: 1.1 39dd8394fd9e6a08c6913ca5845d75f1.cloudfront.net (CloudFront) X-Amz-Cf-Id: 0_r8ooshXGuZ80pSWmlJXucPoL6Pz_Nrm6PpOKZP6vLsesyzgZ1ZiQ== $ curl -I http://dz00r977zpll6.cloudfront.net/subdir/ HTTP/1.1 403 Forbidden Content-Type: application/xml Connection: keep-alive Date: Tue, 24 Apr 2018 14:44:04 GMT Server: AmazonS3 X-Cache: Error from cloudfront Via: 1.1 39dd8394fd9e6a08c6913ca5845d75f1.cloudfront.net (CloudFront) X-Amz-Cf-Id: ZnXZ0P3hX4QIISWN75M6ro26_iBfjZsVu_Ca-h5_bFLSPylKvG4VXg==
今回は Default Root Object
も、静的ウェイブサイトホスティングも使ってないので index.html
は返ってこずに、403 Forbidden
になっていることが確認できました。
Lambda@Edge 用のロール作成
Lambda@Edge 関数に割り当てるロールを作成しておきます。このあたりの設定は、マニュアルに記載されているので、そのまま書いておきます。通常の Lambda 関数と違って、CloudFront ディストリビューションへの関連付け用のアクセス許可が必要となります。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*" } ] }
また、サービスプリンシパル lambda.amazonaws.com
と edgelambda.amazonaws.com
が AssumeRole できる必要があるので、IAM ロール設定の[信頼関係]タブを開いて、[信頼関係の編集]を開き、以下のように設定します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": [ "lambda.amazonaws.com", "edgelambda.amazonaws.com" ] }, "Action": "sts:AssumeRole" } ] }
Lambda@Edge 関数を作成
それでは Lambda コンソールを開いて作成しましょう。
まずリージョンの指定ですが、2018年4月時点で Lambda@Edge 関数 の作成は、バージニアリージョンでしか出来ないようですので、us-east-1(米国東部:バージニア北部)
を選択してください。ランタイムについても同時点での対応は Node.js 6.10
のみになります。次に、事前に作成しておいた Lambda@Edge 用の IAM ロールを指定します(このタイミングで作成しても問題ありません)。指定できましたら[関数の作成]をクリックします。
関数が作成できましたら、以下のコードをコピーして貼り付けましょう。なお、コードの編集はバージョンが $LATEST
でなければ出来ませんので、編集できない場合はバージョンを確認してください。
'use strict'; exports.handler = (event, context, callback) => { // Extract the request from the CloudFront event that is sent to Lambda@Edge var request = event.Records[0].cf.request; // Extract the URI from the request var olduri = request.uri; // Match any '/' that occurs at the end of a URI. Replace it with a default index var newuri = olduri.replace(/\/$/, '\/index.html'); // Log the URI as received by CloudFront and the new URI to be used to fetch from origin console.log("Old URI: " + olduri); console.log("New URI: " + newuri); // Replace the received URI with the URI that includes the index page request.uri = newuri; // Return to CloudFront return callback(null, request); };
その他はデフォルトを受け入れて[保存]します。
トリガーの設定
次に関数のトリガーを設定します。今度はバージョンが $LATEST
だと設定が出来ませんので、[アクション]のプルダウンメニューから[新しいバージョンを発行]を選択します。
バージョンの説明は任意で入力し[発行]をクリックします。
バージョンが変わったことを確認し、左ペインから[CloudFront]を選択します。
画面を下にスクロールし、[トリガーの設定]を行います。ディストリビューションでは事前に作成した CloudFront ディストリビューションを指定し、[CloudFront イベント]は、オリジンリクエスト
を選択します。さいごに、[トリガーとレプリケート]にチェックをいれて[追加]をクリックしましょう。
CloudFront ディストリビューションの Status
を確認すると、In Progress
になり、グローバルの ClouFront へのデプロイが完了するまで少し待ちます。Deployed
に変われば完了です。
アクセスしてみる。Lambda@Edge があるときぃ〜
それでは先ほどと同じ方法でアクセスしてみましょう!
$ curl -I http://dz00r977zpll6.cloudfront.net/ HTTP/1.1 200 OK Content-Type: text/html Content-Length: 46 Connection: keep-alive Date: Wed, 25 Apr 2018 15:05:53 GMT Last-Modified: Wed, 18 Apr 2018 14:42:50 GMT ETag: "cef53ea838dd5ef398d854e599152324" Accept-Ranges: bytes Server: AmazonS3 X-Cache: Miss from cloudfront Via: 1.1 45fd5b8af901a90c70a650138c35291c.cloudfront.net (CloudFront) X-Amz-Cf-Id: gLoeNcXrk9n5BxTaWLMPR1ysaB696A8-R3lDvitFN_-A3o1boJu8lA== $ curl -I http://dz00r977zpll6.cloudfront.net/subdir/ HTTP/1.1 200 OK Content-Type: text/html Content-Length: 48 Connection: keep-alive Date: Wed, 25 Apr 2018 15:06:20 GMT Last-Modified: Wed, 18 Apr 2018 14:43:41 GMT ETag: "ea456df8da39d2bb8f075c99e36ca90c" Accept-Ranges: bytes Server: AmazonS3 X-Cache: Miss from cloudfront Via: 1.1 821834a43f39b878528a4af98ff4016c.cloudfront.net (CloudFront) X-Amz-Cf-Id: auBn-T3pAXrUbGZrUptStW48582a3Q_DuaVvEoVXcZWRmiGytJLMbw==
HTTP ステータスは 200
が返ってくるようになりましたね。きちんとサブディレクトリの index.html
なのか、内容も確認してみましょう。
$ curl -v http://dz00r977zpll6.cloudfront.net/subdir/ * Trying 54.230.124.199... * TCP_NODELAY set * Connected to dz00r977zpll6.cloudfront.net (54.230.124.199) port 80 (#0) > GET /subdir/ HTTP/1.1 > Host: dz00r977zpll6.cloudfront.net > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/html < Content-Length: 48 < Connection: keep-alive < Date: Wed, 25 Apr 2018 15:08:42 GMT < Last-Modified: Wed, 18 Apr 2018 14:43:41 GMT < ETag: "ea456df8da39d2bb8f075c99e36ca90c" < Accept-Ranges: bytes < Server: AmazonS3 < X-Cache: Miss from cloudfront < Via: 1.1 d5145f1eca1e6acd0298623b7a7eeb34.cloudfront.net (CloudFront) < X-Amz-Cf-Id: _Q93RMRRl5q0VD7LkpsOf9kcMMHLS-JNCiyVnL_hKj1_mG7mY-DmQA== < * Connection #0 to host dz00r977zpll6.cloudfront.net left intact <html><body><h1>subdir index!</h1></body></html>
ちゃんと subdir index!
が表示されているので、サブディレクトリの index.html
が返ってますね!
$ curl -I http://dev.classmethod.jp/wp-content/uploads/2019/01/httpstls.oguri_.classmethod.infoindex.html-2019-01-25-06-37-20.png HTTP/1.1 404 Not Found x-amz-error-code: NoSuchWebsiteConfiguration x-amz-error-message: The specified bucket does not have a website configuration x-amz-error-detail-BucketName: cm-rootobject-test x-amz-request-id: 54249ADB18C18A2F x-amz-id-2: tYFAEMWMca8oRgTv1AfaVbzRdF0qrN3RRSPPJS1TN7vAWtLbnTiyCLOcnDfZ2Yd3L7x+FTFBU58= Transfer-Encoding: chunked Date: Thu, 26 Apr 2018 15:42:10 GMT Server: AmazonS3
もちろん、CloudFront をバイパスしたアクセスは出来ません。これで期待したとおりに動いてることが確認できましたね!
まとめ
Lambda@Edge 関数を使い、URL パスを書き換えることでインデックスドキュメントを返す方法をご紹介しました。静的ウェブサイトホスティングではなく、S3 オリジンのまま利用することで、CloudFront をバイパスしたアクセス制限はオリジンアクセスアイデンティティ
にまるっとおまかせできるので、比較的、簡単な方法ではないかと思います!
また、今回 Lambda@Edge 関数を初めて使いましたが、通常の Lambda 関数とは違って少しクセがあるようですね。そのあたりは、以下、大阪オフィス 西村の記事をあわせて読んでいただければ良いかと思います!
以上!大阪オフィスの丸毛(@marumo1981)でした!